package il.technion.cs236369.webserver;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.LinkedList;
import java.util.Properties;
import java.util.Queue;

import javax.net.ServerSocketFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPathExpressionException;

import org.apache.http.params.BasicHttpParams;
import org.xml.sax.SAXException;

import com.google.inject.Guice;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.name.Named;

public class WebServer {

	ServerSocket serverSocket;				// one socket for accepting incoming connections
	ServerSocketFactory serverSocketFactory;	// manufacture server socket
	Queue<Socket> sockets = new LinkedList<Socket>();
	Queue<ExtendedHttpRequest> requests = new LinkedList<ExtendedHttpRequest>();
	LinkedList<SocketReader> socketReaders = new LinkedList<SocketReader>();
	LinkedList<RequestHandler> handlers = new LinkedList<RequestHandler>();
	
	public static String baseDirPath;
	public static String WelcomeFile;
	private int NumSocketReaders;
	private int NumRequestHandlers;
	private int SizeSocketQueue;
	public static int SizeRequestQueue;
	private int portNumber;
	private ServerSocketFactory serverSocketFactor;

	public static Mappings map = null;
	
	/**
	 * Constructs the WebServer
	 * 
	 * @param srvSockFactory
	 *            The ServerSocketFactory to be used for creating a single
	 *            ServerSocket for listening for clients requests
	 * 
	 * @param port
	 *            The port number to bounded by the ServerSocket
	 * 
	 * @param baseDir
	 *            The base directory of the server in the file system (absolute
	 *            path)
	 * @param welcomeFile
	 *            The file should be returned upon request of the base dir
	 * @param numSocketReaders
	 *            number of socket-reader threads
	 * @param numRequestHandlers
	 *            number of request-handler threads
	 * @param sizeSocketQueue
	 *            size of the socket queue (how many cliend sockets are saved)
	 * @param sizeRequestQueue
	 *            size of the socket queue (how many requests are saved)
	 * @param portNumber 
	 */
	@Inject
	WebServer(ServerSocketFactory srvSockFactory,
			@Named("httpserver.net.port") int port,
			@Named("httpserver.app.baseDir") String baseDir,
			@Named("httpserver.app.welcomeFile") String welcomeFile,
			@Named("httpserver.threads.numSocketReaders") int numSocketReaders,
			@Named("httpserver.threads.numRequestHandlers") int numRequestHandlers,
			@Named("httpserver.queues.sizeSocketQueue") int sizeSocketQueue,
			@Named("httpserver.queues.sizeRequestQueue") int sizeRequestQueue) {
		// YOUR IMPLEMENTATION HERE
		baseDirPath = baseDir;
		portNumber = port;
		WelcomeFile = welcomeFile;
		NumSocketReaders = numSocketReaders;
		NumRequestHandlers = numRequestHandlers;
		SizeSocketQueue = sizeSocketQueue; 
		SizeRequestQueue = sizeRequestQueue;
		serverSocketFactor = srvSockFactory;
		
		//parses the first config file - config
		parseConfig();
		
		//parses the second config file - config.xml
		map = new Mappings();
		try {
			map.parseHanlders();
		} catch (XPathExpressionException e) {
			e.printStackTrace();
		} catch (ParserConfigurationException e) {
			e.printStackTrace();
		} catch (SAXException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	private boolean parseConfig()
	{
		File configFile = new File("config");
		Properties pro = new Properties();
		FileInputStream in;
		try
		{
			in = new FileInputStream(configFile);
			pro.load(in);
		} 
		catch (FileNotFoundException e)
		{
			System.err.println("Enable to read config file");
			e.printStackTrace();
			return false;
		}
		catch (IOException e)
		{
			System.err.println("Enable to load config file");
			e.printStackTrace();
			return false;
		}
		try
		{
			baseDirPath = pro.getProperty("httpserver.app.baseDir").toLowerCase();
			int num = Integer.valueOf(pro.getProperty("httpserver.net.port"));
			portNumber = num;
			WelcomeFile = pro.getProperty("httpserver.app.welcomeFile");
			NumSocketReaders = Integer.valueOf(pro.getProperty("httpserver.threads.numSocketReaders"));
			NumRequestHandlers = Integer.valueOf(pro.getProperty("httpserver.threads.numRequestHandlers"));
			SizeSocketQueue = Integer.valueOf(pro.getProperty("httpserver.queues.sizeSocketQueue")); 
			SizeRequestQueue = Integer.valueOf(pro.getProperty("httpserver.queues.sizeRequestQueue"));
		}
		catch(NumberFormatException e)
		{
			System.err.println("Illiegal format in config file, check for errors");
			e.getStackTrace();
		}
		return true;

	}

	/**
	 * Create a new bounded server socket using ServerSocketFactory with the
	 * given port, and fills pools with threads
	 * 
	 * @throws IOException
	 *             unable to bind the server socket
	 */
	public void bind() throws IOException 
	{
		serverSocket =  serverSocketFactor.createServerSocket(portNumber);
		createThreadPools();
	}

	/**
	 * Starts the server loop: listens to client requests and executes them.
	 */
	public void start() {
		
		while(true){
			try{
			listen();
			}
			catch (IOException e) {
				System.err.println("Can't bind to server socket");
				e.printStackTrace();
				System.exit(0);
			}
		}
		
	}

	/**
	 * listen to clients request, when such arrives it puts it into the socket queue and continue listening
	 * @throws IOException
	 *             unable to bind the server socket
	 */
	private void listen() throws IOException
	{
		while (true)
		{
			Socket socket;
			socket = serverSocket.accept();
			boolean notFull = !isSocketQueueFull();
			if (notFull)
			{
				synchronized (sockets)
				{
					sockets.add(socket);
					sockets.notify();
				}
				//System.out.println("socket created");
			} else
			{
				HttpResponser responser = new HttpResponser();
				responser.sendErrorResponse(socket,503,"Service Unavailable","Server is temporarily unavailable, please try again later");
				System.err.println("Queue Socket is full");
			}
			//check if thread pools are full, if not refills them
			fillThreadPools();
		}
	}	

	private boolean isSocketQueueFull()
	{
		return (sockets.size()>=SizeSocketQueue);
	}

	public static void main(String[] args) throws Exception {
		// first arg is the path to the config file
		Properties props = new Properties();
		props.load(new FileInputStream(args[0]));
		Injector injector = Guice.createInjector(new WebServerModule());
		WebServer server = injector.getInstance(WebServer.class);
		server.bind();
		server.start();
	}
	
	/**
	 * Fills the handlers pools
	 * @throws ParserConfigurationException, SAXException, IOException, XPathExpressionException
	 */
	private void createThreadPools()
	{
		for (int i=0;i<NumSocketReaders;i++)
		{
			SocketReader socketReader = new SocketReader(sockets, requests, new BasicHttpParams(),i+1); 
			socketReader.start();
			socketReaders.add(socketReader);
			//System.out.println("socket " + (i+1) +" was created");
		}
		for (int i=0;i<NumRequestHandlers;i++)
		{
			RequestHandler handler = new RequestHandler(requests, i+1); 
			handler.start();
			handlers.add(handler);
			//System.out.println("handler " + (i+1) +" was created");
		}
		
	}
	
	/**
	 * Check whether there are any dead socket readers/request handlers and regenerates them
	 */
	private void fillThreadPools()
	{
		//refilling dead socket readers
		LinkedList<SocketReader> deadSockets = new LinkedList<SocketReader>();
		for (int i=0;i<NumSocketReaders;i++)
		{
			SocketReader socketReader = socketReaders.get(i);
			if (!socketReader.isAlive())
			{
				deadSockets.add(socketReader);
				System.out.println("spotted dead socket");
			}
		}
		for (int i=0;i<deadSockets.size();i++)
		{
			socketReaders.remove(deadSockets.get(i));
		}
		for (int i=0;i<deadSockets.size();i++)
		{
			SocketReader socketReader = new SocketReader(sockets, requests, new BasicHttpParams(),i+1); 
			socketReader.start();
			socketReaders.add(socketReader);
		}
		 
		//refilling dead handlers
		LinkedList<RequestHandler> deadhandler = new LinkedList<RequestHandler>();
		for (int i=0;i<NumRequestHandlers;i++)
		{
			RequestHandler handler = handlers.get(i);
			if (!handler.isAlive())
			{
				deadhandler.add(handler);
			//	System.out.println("spoted dead handler");
			}
		}
		for (int i=0;i<deadhandler.size();i++)
		{
			handlers.remove(deadhandler.get(i));
		}
		for (int i=0;i<deadhandler.size();i++)
		{
			RequestHandler handler = new RequestHandler(requests, i+1); 
			handler.start();
			handlers.add(handler);
			//System.out.println("handler created");
		}
		
	}
	
}
